﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Text.RegularExpressions;
using System.Security.Authentication;

namespace P3SS.Networking
{
	/// <summary>
	/// Performs interactions with an SMTP server
	/// </summary>
	public class SmtpProtocol : AbstractProtocol
	{
		#region Constructors
		/// <summary>
		/// Default constructor. Sets server to localhost:25 with no encryption
		/// </summary>
		public SmtpProtocol()
		{
			server = "localhost";
			port = 25;
			encryption = EncryptionType.None;
			serverCapabilities = "";
		}

		/// <summary>
		/// Copy constructor
		/// </summary>
		/// <param name="inSmtpProtocol">SmtpProtocol object to copy</param>
		public SmtpProtocol(SmtpProtocol inSmtpProtocol)
		{
			server = inSmtpProtocol.Server;
			port = inSmtpProtocol.Port;
			encryption = inSmtpProtocol.Encryption;
			serverCapabilities = inSmtpProtocol.ServerCapabilities;
		}
		#endregion
		#region Properties
		/// <summary>
		/// Username to use when authenticating
		/// </summary>
		public new string Username
		{
			get
			{ throw new NotImplementedException(); }
			set
			{ throw new NotImplementedException(); }
		}

		/// <summary>
		/// Password to use when authenticating
		/// </summary>
		public new string Password
		{
			get
			{ throw new NotImplementedException(); }
			set
			{ throw new NotImplementedException(); }
		}
		#endregion
		#region Private Methods
		/// <summary>
		/// Connects to the SMTP server, encrypts connection and (in future) authenticates
		/// </summary>
		private void connect()
		{
			string myHostName = Dns.GetHostName();

			if (string.IsNullOrEmpty(server))
			{
				throw new ArgumentException("Server can not be blank", "server");
			}
			else if ((port < 1) || (port > 65535))
			{
				throw new ArgumentOutOfRangeException("port", "Port number is invalid");
			}

			innerProtocol = new InnerProtocol(server, port, (encryption == EncryptionType.Implicit));
			string response = innerProtocol.ReadLine();

			if (response.StartsWith("220"))
			{
				response = innerProtocol.WriteCommand("EHLO " + myHostName);
				if (response.StartsWith("250"))
				{
					serverCapabilities = response;
					if (encryption == EncryptionType.Explicit)
					{
						response = innerProtocol.WriteCommand("STARTTLS");
						if (response.StartsWith("220"))
						{
							innerProtocol.UseSSL();
							response = innerProtocol.WriteCommand("EHLO " + myHostName);
							if (response.StartsWith("250"))
							{
								serverCapabilities = response;
							}
							else
							{
								throw new ProtocolErrorException("Server responded to post-STARTTLS EHLO incorrectly", response, "EHLO " + myHostName);
							}
						}
						else
						{
							throw new ProtocolErrorException("Server responded to STARTTLS incorrectly", response, "STARTTLS");
						}
					}
				}
				else if (response.StartsWith("502"))
				{
					// older server, doesn't understand EHLO
					response = innerProtocol.WriteCommand("HELO " + myHostName);
					if (response.StartsWith("250"))
					{
						if (encryption == EncryptionType.Explicit)
						{
							throw new ProtocolErrorException("Can not start Explicit encryption, server does not support ESMTP");
						}
						else
						{
							serverCapabilities = "Can't find capabilities, server doesn't support ESMTP";
						}
					}
					else
					{
						throw new ProtocolErrorException("Server responded incorrectly to HELO", response, "HELO " + myHostName);
					}
				}
				else
				{
					throw new ProtocolErrorException("Server responded incorrectly to EHLO", response, "EHLO " + myHostName);
				}
			}
			else
			{
				throw new ProtocolErrorException("Server responded incorrectly to connection", response, "");
			}
		}

		/// <summary>
		/// Close the connection
		/// </summary>
		private void disconnect()
		{
			string response = innerProtocol.WriteCommand("QUIT");
			innerProtocol.Close();

			if (!response.StartsWith("221"))
			{
				throw new ProtocolErrorException("Server responded to QUIT incorrectly. Connect closed anyway.", response, "QUIT");
			}

			innerProtocol = null;
		}

		/// <summary>
		/// Handles actual mail submission
		/// </summary>
		/// <param name="to">Email address to submit mail to</param>
		/// <param name="from">Email address to deliver mail to</param>
		/// <param name="mail">Mail to deliver (must end in \r\n.\r\n)</param>
		private void innerSubmitMail(string to, string from, string mail)
		{
			string response = innerProtocol.WriteCommand("MAIL FROM:<" + from + ">");
			if (response.StartsWith("250"))
			{
				response = innerProtocol.WriteCommand("RCPT TO:<" + to + ">");
				if ((response.StartsWith("250")) || (response.StartsWith("251")))
				{
					response = innerProtocol.WriteCommand("DATA");
					if (response.StartsWith("354"))
					{
						response = innerProtocol.WriteCommand(mail, false);
						if (!response.StartsWith("250"))
						{
							throw new ProtocolErrorException("Server responded incorrectly to data being sent", response, mail);
						}
					}
					else
					{
						throw new ProtocolErrorException("Server responded incorrectly to DATA", response, "DATA");
					}
				}
				else
				{
					throw new ProtocolErrorException("Server responded incorrectly to RCPT TO", response, "RCPT TO:<" + to + ">");
				}
			}
			else
			{
				throw new ProtocolErrorException("Server responded incorrectly to MAIL FROM", response, "MAIL FROM:<" + from + ">");
			}
		}
		#endregion
		#region Public Methods
		/// <summary>
		/// Submits mail to the server. From address is derived from headers.
		/// </summary>
		/// <param name="toEmailAddress">Email address to submit mail to</param>
		/// <param name="mailToSubmit">Actual message (including headers, body and attachements) to submit</param>
		public void SubmitMail(string toEmailAddress, string mailToSubmit)
		{
			Regex fromRegex = new Regex(@"^\W*from:[^:]*?(""[^""]*"")?[^:]*?([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]*)", RegexOptions.IgnoreCase | RegexOptions.Multiline);

			if (string.IsNullOrEmpty(mailToSubmit))
			{
				throw new ArgumentException("There must be a mail item to submit", "mailToSubmit");
			}
			else if (string.IsNullOrEmpty(toEmailAddress))
			{
				throw new ArgumentException("Mail item must have a to address", "toEmailAddress");
			}
			else if (toEmailAddress.IndexOfAny(new char[] { '\r', '\n', '<', '>' }) > -1)
			{
				throw new ArgumentException("Email address contains invalid character");
			}
			else
			{
				if (!mailToSubmit.EndsWith("\r\n.\r\n"))
				{
					mailToSubmit = string.Format("{0}\r\n.\r\n", mailToSubmit); 
				}

				Match fromMatch = fromRegex.Match(mailToSubmit);
				if (fromMatch.Success)
				{
					// Need to connect and disconnect since SMTP can only have one mail item submitted at once
					connect();
					innerSubmitMail(toEmailAddress, fromMatch.Groups[2].Value, mailToSubmit);
					disconnect();
				}
				else
				{
					throw new ArgumentException("Mail item has no from header", "mailToSubmit");
				}
			}
		}
		#endregion
	}
}
